package com.shutuo.callnummeiwei.core;

import com.shutuo.callnummeiwei.net.PacketConvert;
import com.shutuo.callnummeiwei.net.SocketWire;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

import androidx.annotation.IntDef;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import ashy.earl.common.app.App;
import ashy.earl.common.closure.EarlCall;
import ashy.earl.common.closure.Method0_0;
import ashy.earl.common.closure.Params0;
import ashy.earl.common.data.JsonParser;
import ashy.earl.common.task.MessageLoop;
import ashy.earl.common.task.Task;
import ashy.earl.common.util.L;
import ashy.earl.common.util.ModifyList;
import ashy.earl.common.util.NetworkChangeHelper;
import ashy.earl.common.util.NetworkChangeHelper.NetworkListener;
import ashy.earl.common.util.Util;

import static ashy.earl.common.closure.Earl.bind;


@Keep
public class ChannelClient {
    private static final String TAG = "ChannelClient";
    public static final int STATE_STOP = 1;
    public static final int STATE_WAIT_NETWORK = 2;
    public static final int STATE_CONNECTING = 3;
    public static final int STATE_CONNECTED = 4;
    public static final int STATE_READY = 5;
    private static final int RETRY_DELAY = 10 * 1000;// 10s
    private final PacketConvert mPacketConvert = new PacketConvert(C.CHANNEL);
    private final NetworkChangeHelper mNetworkChangeHelper;
    private final MessageLoop mCreateLoop;
    private final long mClientVersion;
    //
    private String mServerIp;
    private int mServerPort;
    private long mServerVersion;
    @State
    private int mState = STATE_STOP;
    private SocketWire mSocketWire;
    private Task mRetryConnectTask;
    private ModifyList<ClientHandler> mClientListeners = new ModifyList<>();
    private NetworkListener mNetworkListener = new NetworkListener() {
        @Override
        public void onNetworkChanged(boolean hasActiveNetwork, String type, String name) {
            connectIfNeed();
        }
    };
    private SocketWire.WireListener mWireListener = new SocketWire.WireListener() {
        @Override
        public void onConnected(SocketWire wire) {
            wireConnected();
        }

        @Override
        public void onDisconnected(SocketWire wire, int reason, String detail) {
            wireDisconnected(reason, detail);
        }

        @Override
        public void convertPacket(SocketWire wire, SocketWire.Packet packet) {
            mPacketConvert.convertPacket(packet);
        }

        @Override
        public void onNewPacket(SocketWire wire, SocketWire.Packet packet) {
            newMsg(packet.arg0, packet.parsedData);
        }
    };

    @IntDef({STATE_STOP, STATE_WAIT_NETWORK, STATE_CONNECTED, STATE_CONNECTING, STATE_READY})
    @Retention(RetentionPolicy.SOURCE)
    private @interface State {
    }

    public interface ClientHandler {
        void onStateChanged();

        boolean onNewPacket(int type, Object msg);
    }

    public static String stateToString(@State int state) {
        switch (state) {
            case STATE_STOP:
                return "stop";
            case STATE_WAIT_NETWORK:
                return "wait-network";
            case STATE_CONNECTED:
                return "connected";
            case STATE_CONNECTING:
                return "connecting";
            case STATE_READY:
                return "ready";
            default:
                return "unknow-" + state;
        }
    }

    public ChannelClient(long clientVersion) {
        this.mClientVersion = clientVersion;
        mNetworkChangeHelper = NetworkChangeHelper.get();
        mCreateLoop = MessageLoop.current();
        mPacketConvert.registMsgParser(C.MSG_HELLO, C.PARSER_HELLO);
        mPacketConvert.registMsgParser(C.MSG_HELLO_ACK, C.PARSER_HELLO_ACK);
    }

    private void wireConnected() {
        setState(STATE_CONNECTED);
        sendMsgToServer(C.MSG_HELLO, new C.MsgHello(mClientVersion, App.getPkg()));
    }

    private void wireDisconnected(@SocketWire.Reason int reason, String detail) {
        mSocketWire = null;
        switch (reason) {
            // We are disconnecting session, don't retry.
            case SocketWire.REASON_SELF_DISCONNECT:
                return;
            case SocketWire.REASON_PEER_DISCONNECT:
            case SocketWire.REASON_KEEP_ALIVE:
            case SocketWire.REASON_CONNECTITON:
            case SocketWire.REASON_PROTOCOL:
                setState(STATE_CONNECTING);
                if (mNetworkChangeHelper.hasActiveNetwork()) {
                    if (L.loggable(C.CHANNEL, L.DEBUG)) {
                        L.d(C.CHANNEL, "%s~ server disconnected, retry after %dms", TAG,
                                RETRY_DELAY);
                    }
                    if (mRetryConnectTask != null) mRetryConnectTask.cancel();
                    mRetryConnectTask = bind(retryConnect, this).task();
                    mCreateLoop.postTaskDelayed(mRetryConnectTask, RETRY_DELAY);
                } else {
                    if (L.loggable(C.CHANNEL, L.DEBUG)) {
                        L.d(C.CHANNEL, "%s~ server disconnected, hold on by no network", TAG);
                    }
                }
                break;
        }
    }

    @EarlCall
    private void retryConnect() {
        mRetryConnectTask = null;
        connectIfNeed();
    }

    private void newMsg(int type, Object msg) {
        if (msg == null) {
            L.w(C.CHANNEL, "%s~ newMsg from server, null msg, type:%d", TAG, true);
            return;
        }
        if (type == C.MSG_HELLO_ACK) {
            if (mState == STATE_CONNECTED) {
                mServerVersion = ((C.MsgHelloAck) msg).serverVersion;
                setState(STATE_READY);
            }
            return;
        }
        for (ClientHandler l : mClientListeners) {
            if (l.onNewPacket(type, msg)) {
                mClientListeners.finishIterator();
                break;
            }
        }
    }

    public void sendMsgToServer(int type, Object msg) {
        if (mSocketWire == null) {
            L.w(C.CHANNEL, "%s~ sendMsgToServer ignored by socket wire not ready! type:%d, msg:%s",
                    TAG,
                    type, msg);
            return;
        }
        SocketWire.Packet packet = SocketWire.Packet.obtain(SocketWire.Packet.TYPE_MSG, type);
        packet.setParsedData(msg);
        mSocketWire.sendPacket(packet);
    }

    public boolean isReady() {
        return mState == STATE_READY;
    }

    public boolean isStoped() {
        return mState == STATE_STOP;
    }

    public String getServerIp() {
        return mServerIp;
    }

    public long getServerVersion() {
        return mServerVersion;
    }

    @State
    public int getState(){
        return mState;
    }

    public void addClientHandler(ClientHandler handler) {
        mClientListeners.add(handler);
    }

    public void removeClientHandler(ClientHandler handler) {
        mClientListeners.remove(handler);
    }

    public void registMsg(int type, JsonParser parser) {
        if (mServerPort > 0) throw new IllegalAccessError("Must access before setup!");
        mPacketConvert.registMsgParser(type, parser);
    }

    private void setState(@State int state) {
        if (mState == state) return;
        if (L.loggable(C.CHANNEL, L.DEBUG)) {
            L.d(C.CHANNEL, "%s~ state changed: %s -> %s", TAG, stateToString(mState),
                    stateToString(state));
        }
        mState = state;
        for (ClientHandler l : mClientListeners) l.onStateChanged();
    }

    public void setup(String serverIp, int serverPort) {
        if (Util.equals(serverIp, mServerIp) && serverPort == mServerPort) return;
        teardown();
        if (L.loggable(C.CHANNEL, L.DEBUG)) {
            L.d(C.CHANNEL, "%s~ setup: %s:%d", TAG, serverIp, serverPort);
        }
        mServerIp = serverIp;
        mServerPort = serverPort;
        mNetworkChangeHelper.addNetworkListener(mNetworkListener);
        connectIfNeed();
    }

    public void teardown() {
        if (L.loggable(C.CHANNEL, L.DEBUG)) {
            L.d(C.CHANNEL, "%s~ teardown", TAG);
        }
        mServerIp = null;
        mNetworkChangeHelper.removeNetworkListener(mNetworkListener);
        release();
    }

    private void release() {
        if (mSocketWire != null) {
            mSocketWire.release();
            mSocketWire = null;
        }
        if (mRetryConnectTask != null) {
            mRetryConnectTask.cancel();
            mRetryConnectTask = null;
        }
        setState(STATE_STOP);
    }

    private void connectIfNeed() {
        if (!mNetworkChangeHelper.hasActiveNetwork()) {
            release();
            setState(STATE_WAIT_NETWORK);
            return;
        }
        if (mServerIp == null) {
            release();
            setState(STATE_CONNECTING);
            return;
        }
        if (mSocketWire != null) return;
        mSocketWire = new SocketWire(C.CHANNEL, mServerIp, mServerPort, mWireListener,
                C.WIRE_CONFIG);
        setState(STATE_CONNECTING);
    }

    private static final Method0_0<ChannelClient, Void> retryConnect = new Method0_0<ChannelClient, Void>(ChannelClient.class, "retryConnect") {
        @Override
        public Void run(ChannelClient target, @NonNull Params0 params) {
            target.retryConnect();
            return null;
        }
    };
}
